0X00 scrapy 的安装与使用

1.windows 下 scrapy 的安装

windows 下的安装首先需要安装一些依赖库,有些最好使用下载 whl 文件进行本地安装的方法,下面是安装步骤和一些需要本地安装的依赖库

1.wheel

pip3 install wheel

2.lxml

http://www.lfd.uci.edu/~gohlke/pythonlibs/#lxml

3.PyOpenssl

https://pypi.python.org/pypi/pyOpenSSL#downloads

4.Twisted

http://www.lfd.uci.edu/~gohlke/pythonlibs/#twisted

5.Pywin32

https://pypi.org/project/pywin32/#files

6.Scrapy

pip3 install scrapy

2.scrapy 的基本运行测试

按照下图的步骤输入,如果最后没有报错就说明安装成功

此处输入图片的描述

3.补充:anaconda 下的 scrapy 的安装

如果 windows 本地安装有 anaconda 集成环境的话那么安装 scrapy 是极其简单的,只需要下面一条命令就可以了

conda install scarpy

0X01 Scrapy框架基本使用

本节主要是对一个实例网站进行抓取,然后顺带介绍一下 Scrapy 框架的使用,我们的实例网站是 http://quotes.toscrape.com/,这是一个官方提供的实例网站,比较经典

1.分析页面确定爬取思路

我们要抓取的页面很简单如下所示:

此处输入图片的描述

首先页面没有使用任何的动态加载的技术,我们能够使用正则直接匹配,另外我们翻页也能够使用改变 url 的 offset 来实现

思路梳理:

(1)请求第一页得到源代码进行下一步分析
(2)获取首页内容并改变 URL 链接准备下一页的请求
(3)获取下一页源代码
(4)将结果保存为文件格式或者存储进数据库

3.scrapy 的初次使用

创建项目

>>scrapy startproject quotetutorial
>>cd quotetutorial
>>scrapy genspider quotes quotes.toscrape.com

使用 pycharm 打开项目

此处输入图片的描述

定义存储结构

此处输入图片的描述

编写页面解析函数

此处输入图片的描述

使用 scrpay shell 进行交互测试

>>scrapy shell quotes.toscrape.com 

此处输入图片的描述

运行我们的“简陋”的爬虫

>>scrapy crawl quotes

此处输入图片的描述

我们可以看到我们想要抓取的第一页的结果已经大致上输出了

完善我们的爬虫实现每一页的抓取

此处输入图片的描述

将我们爬取到的数据保存

>>scrapy crawl quotes -o quotes.json

此处输入图片的描述

除了能保存成 json 后缀的文件以外,我们还能保存成 jl(jsonline,每一行都是一条 json ),或者是 csv 格式,再或者是 xml 格式等,甚至还支持保存到远程 ftp 服务器的形式

-o ftp://user:pass@ftp.example.com/path/quotes.json

对获取到的数据进行其他的处理

如果有一些 item 是我们不想要的,或者是我们想把 item 保存到数据库的话,上面的方法似乎就不是那么适用了,我们就要借助于 scrapy 给我们提供的另一个组件 pipelines.py 帮我们实现

比如我们现在有这样的需求,我们想把名言超出我们规定的长度的部分删除,并且加上三个省略号

此处输入图片的描述

另外我们如果还想存储进数据库的话,我们还要自己写一个 pipeline

此处输入图片的描述

数据库的设置我们需要在 settings.py 中添加配置项

MONGO_URL = 'localhost'
MONGO_DB = 'quotes'

然后是我们需要在 settings.py 中开启启动 pipeline 的选项,使我们的配置生效

此处输入图片的描述

2.最终代码实现

quotes.py

import...

class QuotesSpider(scrapy.Spider):
    name = 'quotes'
    allowed_domains = ['quotes.toscrape.com']
    start_urls = ['http://quotes.toscrape.com/']

    def parse(self, response):
        quotes = response.css('.quote')
        for quote in quotes:

            #定义接收对象item
            item = QuotetutorialItem()

            text = quote.css('.text::text').extract_first()
            author = quote.css('.author::text').extract_first()
            tags = quote.css('.tags .tag::text').extract()
            item['text'] = text
            item['author'] = author
            item['tags'] = tags
            yield item

        next = response.css('.pager .next a::attr(href)').extract_first()

        #拼接下一页的 URL
        url = response.urljoin(next)
        #使用 scrapy.Request 递归的调用自己实现爬取下一页
        yield scrapy.Request(url=url,callback=self.parse)

items.py

import...
class QuotetutorialItem(scrapy.Item):
    # define the fields for your item here like:
    # name = scrapy.Field()
    text = scrapy.Field()
    author = scrapy.Field()
    tags = scrapy.Field()

pipelines.py

import...
#这个类相当于是返回结果的拦截器
class QuotetutorialPipeline(object):

    def __init__(self):
        self.limit = 50

    def process_item(self, item, spider):
        if item['text']:
            if len(item['text']) >  self.limit:
                item['text'] = item['text'][:self.limit].rstrip() + '...'
            return item
        else:
            # scrapy 特殊的错误处理函数
            return DropItem('Missing Text')

class MongoPipeline(object):

    def __init__(self,mongo_url,mongo_db):
        self.mongo_url = mongo_url
        self.mongo_db = mongo_db

    #这个内置函数能从 settings 里面拿到想要的配置信息
    @classmethod
    def from_crawler(cls,crawler):
        return cls(
            mongo_url = crawler.settings.get('MONGO_URL'),
            mongo_db = crawler.settings.get('MONGO_DB')
        )

    #这个方法是爬虫初始化的时候会执行的方法
    def open_spider(self,spider):
        self.client = pymongo.MongoClient(self.mongo_url)
        self.db = self.client[self.mongo_db]

    #重写该方法实现对数据的数据库存储
    def process_item(self,item,spider):
        name = item.__class__.__name__
        self.db[name].insert(dict(item))
        return item

    def close_spider(self,spider):
        self.client.close()

3.最终运行效果

此处输入图片的描述

0X02 scrapy 命令行详解

这里仅仅说一些我上面没有提到过的,至于上面已经说过的关于项目的创建以及我们的项目的运行我这里就不再赘述

1.genspider 选择生成的爬虫对象的模式

scrapy 在生成爬虫对象的时候可以选择生成的模式,不同的模式会生成不同的爬虫模板,模式的选择如下

λ scrapy genspider -l
Available templates:
  basic
  crawl
  csvfeed
  xmlfeed

λ scrapy genspider -t crawl zhihu www.zhihu.com
Created spider 'zhihu' using template 'crawl' in module:
  testpro.spiders.zhihu

2.check 检查代码的正确性

λ scrapy check

----------------------------------------------------------------------
Ran 0 contracts in 0.000s

OK

3.list 返回项目中所有的 spider 的名称

λ scrapy list
zhihu

4.fecth 快速获取网页返回结果

基本请求

λ scrapy fetch http://www.baidu.com

不需要日志信息

λ scrapy fetch --nolog http://www.baidu.com

返回响应头

λ scrapy fetch --nolog  --headers http://www.baidu.com

拒绝重定向

λ scrapy fetch --nolog  --no-redirect http://www.baidu.com

5.view 使用浏览器快速查看响应

λ scrapy view http://www.baidu.com

注意:

这里浏览器打开的是 dump 到本地的页面文件,而不是直接去访问网站

6.shell 进入命令行交互模式方便调试

λ scrapy shell http://www.baidu.com

7.parse 格式化显示页面的解析结果

λ scrapy parse  http://quotes.toscrape.com -c parse

8.settings 获取配置信息

λ scrapy settings --get MONGO_URL
localhost

9.runspider 运行爬虫文件启动项目

当然运行前需要进入对应的文件目录

λ scrapy runspider quotes.py

10.查看对应的版本

λ  scrapy version -v
Scrapy       : 1.6.0
lxml         : 4.3.3.0
libxml2      : 2.9.5
cssselect    : 1.0.3
parsel       : 1.5.1
w3lib        : 1.20.0
Twisted      : 19.2.0
Python       : 3.7.2 (tags/v3.7.2:9a3ffc0492, Dec 23 2018, 23:09:28) [MSC v.1916 64 bit (AMD64)]
pyOpenSSL    : 19.0.0 (OpenSSL 1.1.1b  26 Feb 2019)
cryptography : 2.6.1
Platform     : Windows-10-10.0.17763-SP0

0X03 scrapy 中选择器的用法

我们使用官方文档提供的实例网站来进行测试,网站的源码如下:

<html>
 <head>
  <base href='http://example.com/' />
  <title>Example website</title>
 </head>
 <body>
  <div id='images'>
   <a href='image1.html'>Name: My image 1 <br /><img src='image1_thumb.jpg' /></a>
   <a href='image2.html'>Name: My image 2 <br /><img src='image2_thumb.jpg' /></a>
   <a href='image3.html'>Name: My image 3 <br /><img src='image3_thumb.jpg' /></a>
   <a href='image4.html'>Name: My image 4 <br /><img src='image4_thumb.jpg' /></a>
   <a href='image5.html'>Name: My image 5 <br /><img src='image5_thumb.jpg' /></a>
  </div>
 </body>
</html>

运行下面代码进入交互模式

scrapy shell https://docs.scrapy.org/en/latest/_static/selectors-sample1.html

scrapy 为我们提供了一个内置的选择器类 Selector ,我们可以通过 response.selector 来进行使用

1.xpath 选择器

(1)xpath 选择器提取文本内容

简单看一下 xptah 的通用语法

nodename    选取此节点的所有子节点。
/    从根节点选取。
//    从匹配选择的当前节点选择文档中的节点,而不考虑它们的位置。
@    选取属性。
.    选取当前节点。
..    选取当前节点的父节点。
*    匹配任何元素节点。
@*    匹配任何属性节点。

/bookstore/book[1]    选取属于 bookstore 子元素的第一个 book 元素。
/bookstore/book[last()]    选取属于 bookstore 子元素的最后一个 book 元素。
/bookstore/book[last()-1]    选取属于 bookstore 子元素的倒数第二个 book 元素。
/bookstore/book[position()<3]    选取最前面的两个属于 bookstore 元素的子元素的 book 元素。
//title[@lang]    选取所有拥有名为 lang 的属性的 title 元素。
//title[@lang='eng']    选取所有 title 元素,且这些元素拥有值为 eng 的 lang 属性。
/bookstore/book[price>35.00]    选取 bookstore 元素的所有 book 元素,且其中的 price 元素的值须大于 35.00。
/bookstore/book[price>35.00]/title    选取 bookstore 元素中的 book 元素的所有 title 元素,且其中的 price 元素的值须大于 35.00。

提取 title 的内容

In [2]: response.selector.xpath('/html/head/title').extract_first(
   ...: )
Out[2]: '<title>Example website</title>'

In [3]: response.selector.xpath('/html/head/title/text()').extract
   ...: _first()
Out[3]: 'Example website'

注意:

我们还可以将上面的命令简写成 response.xpath()

(2)xpath 选择器提取属性内容

我们可以使用 xpath 提取 a 标签的属性 href

In [16]: response.xpath('//a/@href').extract()
Out[16]: ['image1.html', 'image2.html', 'image3.html', 'image4.html', 'image5.html']

2.css 选择器

(1)css 选择器提取文本内容

提取 title 的内容

In [5]: response.selector.css('head > title::text')                
Out[5]: [<Selector xpath='descendant-or-self::head/title/text()' da
ta='Example website'>]                                             

In [6]: response.selector.css('head > title::text').extract_first( 
   ...: )                                                          
Out[6]: 'Example website'       

注意:

我们还可以将上面的命令简写成 response.css()

(2)css 选择器提取属性内容

也可以使用 css 提取 a 标签的属性 href

In [17]: response.css('a::attr(href)').extract()
Out[17]: ['image1.html', 'image2.html', 'image3.html', 'image4.html', 'image5.html']

3.re 正则

我们想匹配冒号后面的内容

In [19]: response.css('a::text').re('Name\\:(.*)')
Out[19]:
[' My image 1 ',
 ' My image 2 ',
 ' My image 3 ',
 ' My image 4 ',
 ' My image 5 ']

4.综合使用

In [10]:  response.xpath('//*[@id="images"]').css('img::attr(src)'
    ...: ).extract()
Out[10]:
['image1_thumb.jpg',
 'image2_thumb.jpg',
 'image3_thumb.jpg',
 'image4_thumb.jpg',
 'image5_thumb.jpg']

如果是使用 extract_frist() 的话,我们可以设置 default 属性,这样查找不存在的结果的时候就可以使用我们设置的 defalut 来输出

In [12]:  response.xpath('//*[@id="images"]').css('img::attr(srcc)
    ...: ').extract_first(default='error')
Out[12]: 'error'
In [20]: response.css('a::text').re_first('Name\:(.*)')
Out[20]: ' My image 1 '

0X04 scrapy 中 spiders 的用法

1.spider 的三个属性

为了创建一个Spider,必须继承 scrapy.Spider 类, 且定义以下两个属性和一个方法:

(1)属性

1.name: 用于区别Spider。 该名字必须是唯一的,您不可以为不同的Spider设定相同的名字。
2.allowed_domains:包含允许此爬虫访问的域的可选列表
3.start_urls: 包含了Spider在启动时进行爬取的url列表。 因此,第一个被获取到的页面将是其中之一。 后续的URL则从初始的URL获取到的数据中提取。

(2)方法

parse() 是spider的一个默认方法。

被调用时,每个初始URL完成下载后生成的 Response 对象将会作为唯一的参数传递给该函数。 该方法负责解析返回的数据(response data),提取数据(生成item)以及生成需要进一步处理的URL的 Request 对象。

实例代码:

import scrapy

class DmozSpider(scrapy.Spider):
    name = "dmoz"
    allowed_domains = ["dmoz.org"]
    start_urls = [
        "http://www.dmoz.org/Computers/Programming/Languages/Python/Books/",
        "http://www.dmoz.org/Computers/Programming/Languages/Python/Resources/"
    ]

    def parse(self, response):
        filename = response.url.split("/")[-2]
        with open(filename, 'wb') as f:
            f.write(response.body)

1.重写 parse 方法实现自定义的输出结果

def parse(self, response):
    quotes = response.css('.quote')
    for quote in quotes:

        #定义接收对象item
        item = QuotetutorialItem()

        text = quote.css('.text::text').extract_first()
        author = quote.css('.author::text').extract_first()
        tags = quote.css('.tags .tag::text').extract()
        item['text'] = text
        item['author'] = author
        item['tags'] = tags
        yield item

    next = response.css('.pager .next a::attr(href)').extract_first()

    #拼接下一页的 URL
    url = response.urljoin(next)
    #使用 scrapy.Request 递归的调用自己实现爬取下一页
    yield scrapy.Request(url=url,callback=self.parse)

可以返回两种类型的结果,一种就是 item ,另一种就是 request 对象实现进一步

2.重写 start_requests 方法实现 post 请求

class HttpbinSpider(scrapy.Spider):
    name = 'httpbin'
    allowed_domains = ['www.httpbin.org']
    start_urls = ['http://www.httpbin.org/post']

    #重写 start_requests 改变请求方式
    def start_requests(self):
        yield scrapy.Request(url='http://www.httpbin.org/post',method='POST',callback=self.parse_post)

    #这里重写了默认的回调函数 parse 
    def parse_post(self,response):
        print('hello',response.status)

注意:

这里还有一个方法是 start_requests() 默认调用的方法
make_requests_from_url(),如果我们直接重写这个方法的话,也能实现类似的效果

3.定义 category 实现运行时传入自定义函数

class HttpbinSpider(scrapy.Spider):
    name = 'httpbin'
    allowed_domains = ['www.httpbin.org']
    start_urls = ['http://www.httpbin.org/post']

    def __init__(self,category=None):
        self.category = category

    def start_requests(self):
        yield scrapy.Request(url='http://www.httpbin.org/post',method='POST',callback=self.parse_post)

    def parse_post(self,response):
        print('hello',response.status,self.category)

运行时使用 -a 参数动态传入 category

scrapy crawl httpbin -a category=picture

注意:

如果是传入多个参数的话每个参数前需要加 -a

0X05 scrapy 中 item pipeline 的用法

item pipeline 顾名思义就是项目管道,我们在抓取到 item 以后需要对其进行进一步处理,比如数据的清洗、重复检查、数据库存储等

0.使用的时候需要在 settings 中设置我们配置的 pipeline

ITEM_PIPELINES = {
   'quotetutorial.pipelines.QuotetutorialPipeline': 300,
'quotetutorial.pipelines.MongoPipeline': 400,

}

1.重写 process_item 实现 item 处理

最主要是重写 process_item 方法,这个方法是对 Item 进行处理的

class QuotetutorialPipeline(object):

    def __init__(self):
        self.limit = 50

    def process_item(self, item, spider):
        if item['text']:
            if len(item['text']) >  self.limit:
                item['text'] = item['text'][:self.limit].rstrip() + '...'
            return item
        else:
            # scrapy 特殊的错误处理函数
            return DropItem('Missing Text')

返回值是 Item 或者是 DropItem

2.实现 open_spider 和 close_spider 方法

这两个是初始化爬虫和关闭爬虫的时候会调用的方法,比如我们可以打开和关闭文件

import json

class JsonWriterPipeline(object):

    def open_spider(self, spider):
        self.file = open('items.jl', 'w')

    def close_spider(self, spider):
        self.file.close()

    def process_item(self, item, spider):
        line = json.dumps(dict(item)) + "\n"
        self.file.write(line)
        return item

3.重写 from_crawler 实现读取配置文件中的配置

class MongoPipeline(object):

    def __init__(self,mongo_url,mongo_db):
        self.mongo_url = mongo_url
        self.mongo_db = mongo_db

    #这个内置函数能从 settings 里面拿到想要的配置信息
    @classmethod
    def from_crawler(cls,crawler):
        return cls(
            mongo_url = crawler.settings.get('MONGO_URL'),
            mongo_db = crawler.settings.get('MONGO_DB')
        )

    #这个方法是爬虫初始化的时候会执行的方法
    def open_spider(self,spider):
        self.client = pymongo.MongoClient(self.mongo_url)
        self.db = self.client[self.mongo_db]

    #重写该方法实现对数据的数据库存储
    def process_item(self,item,spider):
        name = item.__class__.__name__
        self.db[name].insert(dict(item))
        return item

    def close_spider(self,spider):
        self.client.close()

0X06 scrapy 中 Download MiddleWare(下载中间件) 的使用

1.基本介绍

先来看一下下载中间件在全局架构中的位置:

此处输入图片的描述

可以明显的看到其在 request 和 response 的过程中起到了一个拦截和修改的作用,但是其实它有三个方法

(1)处理请求:process_request(request, spider)
(2)处理响应:process_response(request, response, spider)
(3)处理异常:process_exception(request, exception, spider)

2.拦截 request 并修改

我们访问 httpbin.org 可以查看到我们的访问的 IP , 我们可以使用 下载中间件拦截我们的请求实现 IP 地址的伪造

middlewares.py

class ProxyMiddleware(object):

    logger = logging.getLogger(__name__)
    def process_request(self,request,spider):
        self.logger.debug('Using proxy')
        request.meta['proxy'] = 'http://127.0.0.1:1080'

然后我们在 settings 中进行设置

settings.py

DOWNLOADER_MIDDLEWARES = {
   'quotetutorial.middlewares.ProxyMiddleware': 443,
}

3.拦截 response 并修改

class ProxyMiddleware(object):

    logger = logging.getLogger(__name__)
    def process_request(self,request,spider):
        self.logger.debug('Using proxy')
        request.meta['proxy'] = 'http://127.0.0.1:1080'

    def process_response(self,spider,request,response):
        response.status = 204
        return response

4.拦截异常并处理

google.py

class GoogleSpider(scrapy.Spider):
    name = 'google'
    allowed_domains = ['www.google.com']
    start_urls = ['http://www.google.com/']

    #设置请求的超时时间为 10s ,超时会抛出异常
    def make_requests_from_url(self, url):

        self.logger.debug('Try First Time')
        return scrapy.Request(url=url,meta={'download_timeout':10},callback=self.parse,dont_filter=True)

    def parse(self, response):
        print(response.text)

middlewares.py

class ProxyMiddleware(object):

    logger = logging.getLogger(__name__)
    def process_exception(self,request,exception,spider):
        self.logger.debug('Get Exception')
        self.logger.debug('Try Second Time')
        request.meta['proxy'] = 'http://127.0.0.1:1080'
        return request

5.其他

运行代码的时候你可能会发现,在调试信息中会出现很多我们没有定义过得 Middleware ,这实际上是系统自己设置的,我们可以通过下面的命令获取这些内置的middleware

scrapy settings --get=DOWNLOADER_MIDDLEWARES_BASE  

如果我们不想使用这些 middleware 我们可以在 settings 中将其置位 None

λ scrapy settings --get=DOWNLOADER_MIDDLEWARES_BASE
{"scrapy.downloadermiddlewares.robotstxt.RobotsTxtMiddleware": 100, "scrapy.downloadermiddlewares.httpauth.HttpAuthMiddleware": 300, "scrapy.downloadermiddlewares.downloadtimeout.DownloadTimeoutMiddleware": 350, "scrapy.downloadermiddlewares.defaultheaders.DefaultHeadersMiddleware": 400, "scrapy.downloadermiddlewares.useragent.UserAgentMiddleware": 500, "scrapy.downloadermiddlewares.retry.RetryMiddleware": 550, "scrapy.downloadermiddlewares.ajaxcrawl.AjaxCrawlMiddleware": 560, "scrapy.downloadermiddlewares.redirect.MetaRefreshMiddleware": 580, "scrapy.downloadermiddlewares.httpcompression.HttpCompressionMiddleware": 590, "scrapy.downloadermiddlewares.redirect.RedirectMiddleware": 600, "scrapy.downloadermiddlewares.cookies.CookiesMiddleware": 700, "scrapy.downloadermiddlewares.httpproxy.HttpProxyMiddleware": 750, "scrapy.downloadermiddlewares.stats.DownloaderStats": 850, "scrapy.downloadermiddlewares.httpcache.HttpCacheMiddleware": 900}

0X07 Scrapy爬取知乎用户信息实战

1.分析爬取信息确定思路

只要用户不是 0关注0粉丝,那么我们就能对与用户关联的人进行递归抓取,这样就能获得源源不断的信息,以轮子哥的知乎为例,我们从控制台看一下他关注的人的信息是怎么加载的,除了第一页是通过在页面中的 json 数据进行的初始化以外,其他几页可以看到是通过 XHR 请求获取的 json 数据,然后是对于每一个用户来讲信息来自于用户信息页面本身的 json 数据

思路梳理:

(1)选定一个关注数或者粉丝数比较多的大 V 作为我们爬取的起点
(2)通过知乎的接口获取大 V 的关注列表和粉丝列表
(3)通过知乎的接口获取关注列表和粉丝列表中用户的信息
(4)对这些用户递归调用爬取其关注列表和粉丝列表

2.代码实现

zhihu.py

class ZhihuSpider(scrapy.Spider):
    name = 'zhihu'
    allowed_domains = ['www.zhihu.com']
    start_urls = ['http://www.zhihu.com/']

    #设置开始用户
    start_user = 'Talyer-Wei'

    #设置查看用户信息的 URL
    user_url = 'https://www.zhihu.com/api/v4/members/{user}?include={include}'
    user_query = 'allow_message,is_followed,is_following,is_org,is_blocking,employments,answer_count,follower_count,articles_count,gender,badge[?(type=best_answerer)].topics'

    #设置查看关注着信息的 URL
    followee_url = 'https://www.zhihu.com/api/v4/members/{user}/followees?include={include}&offset={offset}&limit={limit}'
    followee_query = 'data[*].answer_count,articles_count,gender,follower_count,is_followed,is_following,badge[?(type=best_answerer)].topics'

    def start_requests(self):
        #url = 'https://www.zhihu.com/api/v4/members/xu-zhou-yang-52?include=allow_message%2Cis_followed%2Cis_following%2Cis_org%2Cis_blocking%2Cemployments%2Canswer_count%2Cfollower_count%2Carticles_count%2Cgender%2Cbadge%5B%3F(type%3Dbest_answerer)%5D.topics'
        #url = 'https://www.zhihu.com/api/v4/members/Talyer-Wei/followees?include=data%5B*%5D.answer_count%2Carticles_count%2Cgender%2Cfollower_count%2Cis_followed%2Cis_following%2Cbadge%5B%3F(type%3Dbest_answerer)%5D.topics&offset=0&limit=20'
        #分别请求初始用户的信息和他关注的用户列表
        yield Request(self.user_url.format(user=self.start_user,include=self.user_query),self.parse_user)
        yield Request(self.followee_url.format(user=self.start_user,include=self.followee_query,offset=0,limit=20),self.parse_followee)


    def parse_user(self, response):
        #用请求得到的 json 给我们的 field 赋值
        result = json.loads(response.text)
        item = UserItem()
        for field in item.fields:
            if field in result.keys():
                item[field] = result.get(field)
        yield item
        yield Request(self.followee_url.format(user=result.get('url_token'),include=self.followee_query,limit=20,offset=0),self.parse_followee)


    def parse_followee(self,response):
        #解析出每一个 followee 的用户 url_token
        results = json.loads(response.text)
        if 'data' in results.keys():
            for result in results.get('data'):
                yield Request(self.user_url.format(user=result.get('url_token'),include=self.user_query),self.parse_user)
        if 'paging' in results.keys() and results.get('paging').get('is_end') == False:
            next_page = results.get('paging').get('next')
            yield Request(next_page,self.parse_followee)

item.py

from scrapy import Item,Field

class UserItem(Item):
    # define the fields for your item here like:
    # name = scrapy.Field()
    id = Field()
    name = Field()
    headline = Field()
    url = Field()
    url_token = Field()
    answer_count = Field()
    articles_count = Field()
    avatar_url = Field()
    follower_count = Field()

pipelines.py

import pymongo

class MongoPipeline(object):

    collection_name = 'scrapy_items'

    def __init__(self, mongo_uri, mongo_db):
        self.mongo_uri = mongo_uri
        self.mongo_db = mongo_db

    @classmethod
    def from_crawler(cls, crawler):
        return cls(
            mongo_uri=crawler.settings.get('MONGO_URI'),
            mongo_db=crawler.settings.get('MONGO_DATABASE')
        )

    def open_spider(self, spider):
        self.client = pymongo.MongoClient(self.mongo_uri)
        self.db = self.client[self.mongo_db]

    def close_spider(self, spider):
        self.client.close()

    def process_item(self, item, spider):
        #self.db[self.collection_name].insert_one(dict(item))
        self.db['user'].update({'url_token':item['url_token']},{'$set':item},True )
        return item

注意:

settings 中还要配置 UA 以及数据库的一些常量,这里就不在多写了

0X08 Scrapy分布式原理及Scrapy-Redis源码解析

1.单机 Scrapy 架构和分布式对比

此处输入图片的描述

具体的步骤是 scrapy 引擎通过调度器调度一个队列,发出 requests 请求给 downloader 然后请求网络,但是这个队列都是本机的队列,因此如果要做多台主机的协同的爬取的话,每台主机自己的队列是不能满足我们的需要的,那我们就要将这个队列做成统一的可访问的队列(共享爬取队列),每次调用 requests 的时候都是统一调用这个队列,进行统一的存取操作

此处输入图片的描述

此处输入图片的描述

2.队列使用什么维护

推荐使用 Redis 作为我们的维护队列,原因有一下三点

(1)非关系型数据库,key-value 存储结构灵活
(2)内存中的数据结构存储系统,性能好
(3)提供队列,集合等多种存储结构方便队列维护

3.队列如何去重

使用 redis 的集合数据结构,向集合中加入 requests 的指纹,每一个 requests 加入集合前先验证指纹存不存在集合中,如果存在则不进行加入

4.架构的实现

实际上存在 Scrapy-Redis 这个库,这个库帮我们完美的实现了这个架构,包括调度器、队列、去重等一应俱全

项目地址:https://github.com/rmax/scrapy-redis

5.实际的使用

我们可以将配置好的代码上传到我们的 git 仓库,然后每一台主机去克隆运行

在太多主机的情况下如果觉得这种方式不是很方便的话,github 还有一个 scrapyd 的项目,可以帮助我们部署